import { nameToCaml, nameToClassName, nameToLowerCaseCaml, OnlyEnglishAndNumberExg } from "./OpenApiUtils"


export async function load(apiDocUrl: string, options: OpenApiOptions = {
  host: '',
  nameToCaml: (name) => name,
  project: null
}) {
    var openapi = new OpenApi(apiDocUrl, options)
    await openapi.init()
    console.log(openapi)
    return openapi
}

interface OpenApiOptions {
    host: string
    nameToCaml:  (name: string) => string
    /**
    {
        dirs: {
            controllers: 'src/controllers',
            definitions: 'src/definitions',
        }
     }
     */
    project: Project
}

interface Project {
    name: string
    dirs: {
        controllers: string
        definitions: string
    }
}

export class OpenApi {
    controllers: ApiController[]
    definitions: Definition[]
    apiDocUrl: string
    options: OpenApiOptions
    apiDoc: any
    refMap: any
    refNamePrefix: string
    constructor(apiDocUrl: string, options: OpenApiOptions) {
        this.apiDocUrl = apiDocUrl
        this.options = options
        this.refNamePrefix = '#/definitions/'
    }

    async init() {
        await this._fetchApiDoc()
        this._paraseDefinitions()
        this._parseControllers()
    }

    async _fetchApiDoc() {
        var apiDoc = await fetch(this.apiDocUrl).then((res) => res.json())
        this.apiDoc = apiDoc
    }

    _paraseDefinitions() {
      this.refMap = {}
      if(this.isV3()) {
        this.refNamePrefix = '#/components/schemas/'
      }

      var configDefinitions = this.getDefinitions()
      var definitions: Definition[] = []
      var refs = []
      Object.keys(configDefinitions).forEach((_className)=> {
        var definition = new Definition(this, _className, configDefinitions[_className])
        if(definition.refName) {
          this.setRef(definition.refName, definition)
        }
        refs = refs.concat(definition.refs)
        definitions.push(definition);
      })

      definitions.forEach(def => def.genDefinitions())

      this.definitions = definitions
    }

    _parseControllers() {
        var apis = [];
        var tagsMap = {}
        var tags = this.apiDoc.tags || []
        tags.forEach((tag: any) => {
          if(!tag.name || !tag.description) return
          tagsMap[tag.name] = tag.description.replace(/\s/g, '')
        })
      
        Object.keys(this.apiDoc.paths).forEach((_path)=> {
            Object.keys(this.apiDoc.paths[_path]).forEach((method) => {
                var apiInstance = this.apiDoc.paths[_path][method];
                apiInstance.basePath = this.apiDoc.basePath
                apiInstance.host = this.options.host
                apiInstance.tagsMap = tagsMap
                var api = new ApiMethod(this, method, _path, apiInstance)
                apis.push(api);
            })
        })
      
        var classList = {}
        apis.forEach((_api: ApiMethod)=> {
          if(classList[_api.controllerName] == null)
            classList[_api.controllerName] = [];
          classList[_api.controllerName].push(_api)
        })
        var controllers: ApiController[] = []
        Object.keys(classList).map(key=> {
          var refs = [];
          classList[key].map((item: ApiMethod)=> {
            refs = refs.concat(item.refs || []);
          })

          var controllerName = nameToCaml(key, this.options.nameToCaml)
          var arrayRefs =  Array.from(new Set(refs)).map((ref)=> this.findRef(ref))
          
          controllers.push(new ApiController(this, controllerName, arrayRefs, classList[key]))
        })
        controllers.sort((a, b) => a.sortValue.localeCompare(b.sortValue))
        this.controllers = controllers
    }

    findMethod(controllerName: string, methodName: string) {
      var controller = this.controllers.find(ctrl => ctrl.name === controllerName)
      if(!controller) return null
      var method = controller.methods.find((method) => method.name === methodName)
      return method
    }

    findRef(refName: string): Definition | null {
        return this.refMap[refName];
    }

    setRef(refName: string, ref: Definition) {
        this.refMap[refName] = ref
    }

    getDefinitions() {
      if(this.isV2()) return this.apiDoc.definitions
      if(this.isV3()) return this.apiDoc.components.schemas
      return []
    }
    version() {
      return this.apiDoc.swagger || this.apiDoc.openapi
    }

   isV2() {
      const version = this.version()
      return /^(2.\d+)/.test(version)
    }
    
   isV3() {
    const version = this.version()
    return /^(3.\d+)/.test(version)
  }

  toJsonObject() {
    var refMap = {}

    Object.keys(this.refMap).forEach((key) => {
      var definition = this.refMap[key] as Definition;
      refMap[key] = definition?.toJsonObject()
    })

    return {
      controllers: this.controllers.map((controller) => controller.toJsonObject()),
      definitions: this.definitions.map((definition) => definition.toJsonObject()),
      refMap: refMap
    }
  }

  toJson() {
    return JSON.stringify(this.toJsonObject())
  }
}

class ApiController {
    definitions: Definition[]
    methods: ApiMethod[]
    openapi: OpenApi
    name: string
    tag: string

    constructor(openapi: OpenApi, name: string, definitions: Definition[], methods: ApiMethod[]) {
        this.openapi = openapi
        this.name = name
        this.definitions = definitions
        this.methods = methods
        if(this.methods.length > 0) {
          this.tag = this.methods[0].controllerTagName
        }
    }

    get sortValue() {
      return this.tag || this.name
    }

    toJsonObject(): any {
      return {
        definitions: this.definitions.map((item: Definition) => item.toJsonObject()),
        name: this.name,
        tag: this.tag,
        methods: this.methods.map((item: ApiMethod) => item.toJsonObject())
      }
    }
}

export class ApiMethod {
    host: string
    path: string
    method: string
    queryParams: ReqeustParam[]
    bodyParam: RequestBody
    response: Definition
    pathVars: PathVars
    openapi: OpenApi
    controller: ApiController
    data: ApiMethodOptions
    controllerName: string
    controllerTagName: string
    refs: string[]
    tag: string
    summary: string
    consumes: any
    name: string
    parameters: any[]
    requestBody: any
    responses: any
    basePath: string
    definitions: Definition[]

    constructor(openapi: OpenApi, method: string, path: string, options: ApiMethodOptions) {
        this.path = path
        this.method = method
        this.data = options
        this.openapi = openapi
        this.init()
    }

    init() {
        this.queryParams = [];
        this.bodyParam = null;
        this.refs = [];
        var apiInstance = this.data

        this.tag = apiInstance.tagsMap[apiInstance.tags[0]] || apiInstance.tags[0];
        this.summary = apiInstance.summary;
        this.consumes = apiInstance.consumes;
        this.name = nameToLowerCaseCaml(apiInstance.operationId || this.path.split('/').pop());
        OnlyEnglishAndNumberExg.lastIndex = 0
        const isOnly = OnlyEnglishAndNumberExg.test(this.tag)
        this.controllerName = isOnly ? nameToCaml(this.tag, this.openapi.options.nameToCaml) : (this.path.split("/")[1] + 'Controller');
        this.controllerTagName = apiInstance.tags[0]
        this.parameters = apiInstance.parameters;
        if(apiInstance.requestBody) {
          this.requestBody = apiInstance.requestBody['content']['application/json']
        }
        this.responses = apiInstance.responses;
        this.basePath = apiInstance.basePath || ""
        this.host = apiInstance.host || ""
        if(this.basePath == "/") this.basePath = ""
        var schemaRef = this.getSchemaRef()
        if(schemaRef) this.refs.push(schemaRef)
        if(this.parameters) {
          /// required 优先排在前面
          this.parameters = this.parameters.sort((a, b)=> {
            if(b.required) return 1;
            if(a.required) return -1;
            return 0;
          })
          this.parameters.forEach(param=> {
            if(param.in == "query") {
              this.queryParams.push(param);
            }
            if(param.in == "body" && (param.schema.$ref || param.schema.type)) {
              if(!this.bodyParam) {
                this.bodyParam = param;
              }
            }
            if(param.schema && param.schema.$ref) {
              this.refs.push(param.schema.$ref)
            }
          })
        }
        if(this.requestBody) {
          var param = this.requestBody
          if(param.schema.$ref || param.schema.type) {
            this.bodyParam = param;
          }
          if(param.schema && param.schema.$ref) {
            this.refs.push(param.schema.$ref)
          }
        }
    }
    getSchemaRef() {
        if(this.responses[200].schema) {
            return this.responses[200].schema.$ref
        }
        if(this.responses[200].content && this.responses[200].content["*/*"].schema) {
            return this.responses[200].content["*/*"].schema.$ref
        }
        return null
    }

    parse() {
      if(this.bodyParam && this.bodyParam.schema.$ref) {
        const body = this.openapi.findRef(this.bodyParam.schema.$ref)
        this.bodyParam.body = body
      }
      let response = this.openapi.findRef(this.getSchemaRef())
      this.response = response
      this.parsePathVars()
    }

    parsePathVars() {
      var paramsRegex = /(\{\w+\})/gm;
      var params = this.path.match(paramsRegex) || [];
      
      var properties: Properties = {};
      for(var i=0;i<params.length;i++) {
        var param = params[i].replace(/[\{\}]/g, "");
        properties[param] = {
          type: 'string',
          description: ''
        }
      }
      var pathVars = new PathVars(this.openapi, this.controllerName+"PathVar", {
        properties: properties
      })
      this.pathVars = pathVars
    }

    genDefinitions() {
      this.definitions =  this.refs.map((ref) => {
        var definition = this.openapi.findRef(ref)
        return definition
      }).filter((item) => !!item)
    }

    toJsonObject(): any {
      this.parse()
      this.genDefinitions()
      return {
        host: this.host,
        path: this.path,
        method: this.method,
        tag: this.tag,
        summary: this.summary,
        consumes: this.consumes,
        name: this.name,
        requestBody: this.requestBody,
        responses: this.responses,
        basePath: this.basePath,
        queryParams: this.queryParams,
        bodyParam: this.bodyParam ? {
          ...this.bodyParam,
          body: this.bodyParam?.body?.toJsonObject()
        } : null,
        pathVars: this.pathVars?.toJsonObject(),
        response: this.response?.toJsonObject(),
        controllerName: this.controllerName,
        controllerTagName: this.controllerTagName,
        definitions: this.definitions?.map((item) => item.toJsonObject()),
      }
    }
}

interface ApiMethodOptions {
    basePath: string
    host: string
    tagsMap: any
    tags: string[]
    summary: string
    consumes: any
    operationId: string
    parameters: any[]
    requestBody: any
    responses: any
}

type Property = {
  type: string
  description: string
  format?: string
  enum?: string[]
  $ref?: string
  items?: Property
}

export type Properties = {
  [key: string]: Property
}

export class DefinitionData {
  properties: Properties
}

export class Definition {
    definitions: Definition[]
    refName: string
    openapi: OpenApi
    className: string
    data: any
    refs: string[]
    isMap: boolean
    noRef: boolean
    mapClassName: string
    originClassName: string
    properties: Properties
    findRef: (ref: string) => any
    constructor(openapi: OpenApi, _className: string, data: DefinitionData) {
      this.openapi = openapi
      this.noRef = false

      if(_className.startsWith('Map')) {
        this.isMap = true
        this.noRef = true
        this.mapClassName = nameToClassName(_className)
      }
      this.originClassName = _className
      this.className = nameToCaml(_className, this.openapi.options.nameToCaml)
      this.data = data;
      this.refs = []
      this.refName = `${this.openapi.refNamePrefix}${_className}`;
      this.properties = data.properties as Properties
      this.findRef = function(ref: string) {
        var _ref = this.openapi.findRef(ref)
        if(!_ref) return null
        return _ref
      }
      if(!this.noRef) {
        Object.keys(data.properties || {}).forEach((propertyName) => {
          var property = data.properties[propertyName];
          if(property["$ref"]) {
            this.refs.push(property["$ref"])
          }
          if(property.type === "array" && property.items.$ref) {
            this.refs.push(property.items.$ref)
          }
        })
        this.refs = Array.from(new Set(this.refs))
      }
    }
    genDefinitions() {
      if(!this.noRef) {
        this.definitions =  this.refs.map((ref) => {
          var definition = this.findRef(ref)
          if(definition.refName === this.refName) return null
          return definition
        }).filter((item) => !!item)
      }
    }

    toJsonObject(): any {
      this.genDefinitions()
      return {
        className: this.className,
        refName: this.refName,
        properties: this.properties,
        originClassName: this.originClassName,
        noRef: this.noRef,
        isMap: this.isMap,
        definitions: this.definitions.map((item) => item.toJsonObject()),
        data: this.data,
        mapClassName: this.mapClassName
      }
    }
}
type ReqeustParam = {
  description: string
  in: string
  name: string
  required: boolean
  type: string
}

class RequestBody extends Definition {
  schema: {
    $ref: string
  }
  body?: Definition
}

class PathVars extends Definition {
}